#include "PreCompile.hpp"
#include "OutStream.hpp"
#include "Utils.hpp"

namespace BuildNG
{
  typedef std::string OutStreamException;

  //////////
  // OutStream
  OutStream::OutStream(const Settings &settings)
    : settings(settings)
  {
    assert(!settings.insert_bom && "BOM not implemented yet");
    assert(settings.text_format == TextFormat_UTF8 && "Only UTF8 supported atm.");

    write_ptr = buffer;
  }

  OutStream::~OutStream()
  {
    assert(write_ptr == buffer && "A class derived from OutStream must call flush in it's dtor");
  }

  void OutStream::write_uint32(int v)
  {
    if(v < 0 || (guint32)v > 0xffffffff)
      throw OutStreamException("Value is out of range for an uint32");

    if(write_ptr + 4 > buffer + BUFFER_SIZE)
      flush();
    *((guint32*)write_ptr) = (guint32)v;
    write_ptr += 4;
  }

  void OutStream::write_uint32(guint32 v)
  {
    if(write_ptr + 4 > buffer + BUFFER_SIZE)
      flush();
    *((guint32*)write_ptr) = (guint32)v;
    write_ptr += 4;
  }

  void OutStream::write_int32(int v)
  {
    if(v < (int)0x80000000 || v > 0x7fffffff)
      throw OutStreamException("Value is out of range for an int32");

    if(write_ptr + 4 > buffer + BUFFER_SIZE)
      flush();
    *((gint32*)write_ptr) = (gint32)v;
    write_ptr += 4;
  }

  void OutStream::write_uint16(int v)
  {
    if(v < 0 || v > 0xffff)
      throw OutStreamException("Value is out of range for an uint16");

    if(write_ptr + 2 > buffer + BUFFER_SIZE)
      flush();
    *((guint16*)write_ptr) = (guint16)v;
    write_ptr += 2;
  }

  void OutStream::write_int16(int v)
  {
    if(v < -0x8000 || v > 0x7fff)
      throw OutStreamException("Value is out of range for an int32");

    if(write_ptr + 2 > buffer + BUFFER_SIZE)
      flush();
    *((gint16*)write_ptr) = (gint16)v;
    write_ptr += 2;
  }

  void OutStream::write_uint8(int v)
  {
    if(v < 0 || v > 0xff)
      throw OutStreamException("Value is out of range for an uint8");
    *(write_ptr++) = (guint8)v;

    if(write_ptr == buffer + BUFFER_SIZE)
    {
      flush();
    }
  }

  void OutStream::write_int8(int v)
  {
    if(v < -128 || v > 127)
      throw OutStreamException("Value is out of range for an int8");
    *(write_ptr++) = (guint8)v;

    if(write_ptr == buffer + BUFFER_SIZE)
    {
      flush();
    }
  }

  void OutStream::write_bool(bool v)
  {
    write_uint8(v ? 1 : 0);
  }

  void OutStream::write_line(const Glib::ustring &v)
  {
    const gchar *c_str = v.c_str();

    const gchar *segment_begin = c_str;
    const gchar *c;
    for(c = c_str;*c;c++)
    {
      if(*c == '\n')
      {
        write_buffer(segment_begin,(int)(c - segment_begin));

        write_newline();

        segment_begin = c + 1;
      }
    }

    write_buffer(segment_begin,(int)(c - segment_begin));
    write_newline();
  }

  void OutStream::write_newline()
  {
    switch(settings.line_ending_format)
    {
    case LineEndingFormat_Lf:
      write_uint8('\n');
      break;

    case LineEndingFormat_CrLf:
      write_uint8('\r');
      write_uint8('\n');
      break;

    case LineEndingFormat_LfCr:
      write_uint8('\n');
      write_uint8('\r');
      break;
    }
  }

  void OutStream::write_buffer(const void *data,int buffer_size)
  {
    const char *read_ptr = (const char*)data;

    int size_left = buffer_size;
    int first_write_size = std::min<int>(size_left,(int)(BUFFER_SIZE - (write_ptr - buffer)));
    memcpy(write_ptr,read_ptr,first_write_size);
    read_ptr += first_write_size;
    write_ptr += first_write_size;
    size_left -= first_write_size;

    if(write_ptr == buffer + BUFFER_SIZE)
    {
      write_impl(buffer,BUFFER_SIZE);
      write_ptr = buffer;
    }

    if(size_left == 0)
      return;

    if(size_left > BUFFER_SIZE)
      write_impl(read_ptr,size_left);
    else
    {
      memcpy(write_ptr,read_ptr,size_left);
      write_ptr += size_left;
    }
  }

  void OutStream::flush()
  {
    if(write_ptr - buffer == 0)
      return;

    write_impl(buffer,(int)(write_ptr - buffer));
    write_ptr = buffer;
  }

  //////////
  // FileOutStream
  FileOutStream::FileOutStream(const Glib::ustring &file_name)
  {
    file = fopen(file_name.c_str(),"wb");
    if(!file)
      throw OutStreamException("Couldn't open \"" + file_name + "\" for writing.");
  }

  FileOutStream::~FileOutStream()
  {
    flush();

    assert(file);
    fclose(file);
  }

  void FileOutStream::write_impl(const char *data,int buffer_size)
  {
    size_t num_written = fwrite(data,1,buffer_size,file);
    if(num_written != buffer_size)
      throw OutStreamException("Couldn't write to the file.");
  }

  ////////////
  // MemoryOutStream
  MemoryOutStream::MemoryOutStream()
  {
    size = 0;
    reserved = 0;
    buffer = 0;
  }

  MemoryOutStream::~MemoryOutStream()
  {
    flush();
    free(buffer);
  }

  void MemoryOutStream::write_impl(const char *data,int buffer_size)
  {
    if(size + buffer_size > reserved)
    {
      reserved = std::max(get_next_pow2_size_t(size + buffer_size),reserved * 2);
      buffer = realloc(buffer,reserved);
    }

    memcpy((char*)buffer + size,data,buffer_size);
    size += buffer_size;
  }

  int MemoryOutStream::get_buffer_size() const
  {
    const_cast<MemoryOutStream*>(this)->flush();
    return (int)size;
  }

  const void* MemoryOutStream::get_buffer() const
  {
    const_cast<MemoryOutStream*>(this)->flush();
    return buffer;
  }

  /////////
  // GIOChannelOutStream
  GIOChannelOutStream::GIOChannelOutStream(Glib::RefPtr<Glib::IOChannel> io_channel)
    : io_channel(io_channel)
  {
  }

  GIOChannelOutStream::~GIOChannelOutStream()
  {
    flush();
  }

  void GIOChannelOutStream::write_impl(const char *data,int buffer_size)
  {
    assert(buffer_size != 0);

    gsize total_written = 0;

    do
    {
      gsize cur_bytes_written;
      Glib::IOStatus status = io_channel->write(data + total_written,
        buffer_size - total_written,cur_bytes_written);
      if(status == Glib::IO_STATUS_AGAIN)
        cur_bytes_written = 0;
      else if(status == Glib::IO_STATUS_ERROR)
        throw OutStreamException("Couldn't write data to the GIOChannel.");

      assert(status == Glib::IO_STATUS_NORMAL);

      total_written += cur_bytes_written;
      assert(total_written <= (gsize)buffer_size);
    }
    while(total_written != (gsize)buffer_size);
  }

  /////////
  // StdOutStream
  StdOutStream::StdOutStream(std::ostream *stream,bool take_ownership)
  {
    this->stream = stream;
    owned = take_ownership;
  }

  StdOutStream::~StdOutStream()
  {
    if(owned)
      delete stream;
  }

  void StdOutStream::write_impl(const char *data,int buffer_size)
  {
    stream->write(data,buffer_size);
    if(stream->bad())
      throw OutStreamException("Couldn't write to file.");
  }
}

